/*
 * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
 * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using QuantConnect.Algorithm.Framework.Alphas.Serialization;
using QuantConnect.Interfaces;
using QuantConnect.Securities;

namespace QuantConnect.Algorithm.Framework.Alphas
{
    /// <summary>
    /// Defines a alpha prediction for a single symbol generated by the algorithm
    /// </summary>
    /// <remarks>
    /// Serialization of this type is delegated to the <see cref="InsightJsonConverter"/> which uses the <see cref="SerializedInsight"/> as a model.
    /// </remarks>
    [JsonConverter(typeof(InsightJsonConverter))]
    public class Insight
    {
        private readonly IPeriodSpecification _periodSpecification;

        /// <summary>
        /// Gets the unique identifier for this insight
        /// </summary>
        public Guid Id { get; protected set; }

        /// <summary>
        /// Gets the group id this insight belongs to, null if not in a group
        /// </summary>
        public Guid? GroupId { get; protected set; }

        /// <summary>
        /// Gets an identifier for the source model that generated this insight.
        /// </summary>
        public string SourceModel { get; set; }

        /// <summary>
        /// Gets the utc time this insight was generated
        /// </summary>
        /// <remarks>
        /// The algorithm framework handles setting this value appropriately.
        /// If providing custom <see cref="Insight"/> implementation, be sure
        /// to set this value to algorithm.UtcTime when the insight is generated.
        /// </remarks>
        public DateTime GeneratedTimeUtc { get; set; }

        /// <summary>
        /// Gets the insight's prediction end time. This is the time when this
        /// insight prediction is expected to be fulfilled. This time takes into
        /// account market hours, weekends, as well as the symbol's data resolution
        /// </summary>
        public DateTime CloseTimeUtc { get; set; }

        /// <summary>
        /// Gets the symbol this insight is for
        /// </summary>
        public Symbol Symbol { get; private set; }

        /// <summary>
        /// Gets the type of insight, for example, price insight or volatility insight
        /// </summary>
        public InsightType Type { get; private set; }

        /// <summary>
        /// Gets the initial reference value this insight is predicting against. The value is dependent on the specified <see cref="InsightType"/>
        /// </summary>
        public decimal ReferenceValue { get; set; }

        /// <summary>
        /// Gets the final reference value, used for scoring, this insight is predicting against. The value is dependent on the specified <see cref="InsightType"/>
        /// </summary>
        public decimal ReferenceValueFinal { get; set; }

        /// <summary>
        /// Gets the predicted direction, down, flat or up
        /// </summary>
        public InsightDirection Direction { get; private set; }

        /// <summary>
        /// Gets the period over which this insight is expected to come to fruition
        /// </summary>
        public TimeSpan Period { get; internal set; }

        /// <summary>
        /// Gets the predicted percent change in the insight type (price/volatility)
        /// </summary>
        public double? Magnitude { get; private set; }

        /// <summary>
        /// Gets the confidence in this insight
        /// </summary>
        public double? Confidence { get; private set; }

        /// <summary>
        /// Gets the portfolio weight of this insight
        /// </summary>
        public double? Weight { get; private set; }

        /// <summary>
        /// Gets the most recent scores for this insight
        /// </summary>
        public InsightScore Score { get; protected set; }

        /// <summary>
        /// Gets the estimated value of this insight in the account currency
        /// </summary>
        public decimal EstimatedValue { get; set; }

        /// <summary>
        /// The insight's tag containing additional information
        /// </summary>
        public string Tag { get; protected set; }

        /// <summary>
        /// Determines whether or not this insight is considered expired at the specified <paramref name="utcTime"/>
        /// </summary>
        /// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
        /// <returns>True if this insight is expired, false otherwise</returns>
        public bool IsExpired(DateTime utcTime)
        {
            return CloseTimeUtc < utcTime;
        }

        /// <summary>
        /// Determines whether or not this insight is considered active at the specified <paramref name="utcTime"/>
        /// </summary>
        /// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
        /// <returns>True if this insight is active, false otherwise</returns>
        public bool IsActive(DateTime utcTime)
        {
            return !IsExpired(utcTime);
        }

        /// <summary>
        /// Expire this insight
        /// </summary>
        /// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
        public void Expire(DateTime utcTime)
        {
            if (IsActive(utcTime))
            {
                CloseTimeUtc = utcTime.Add(-Time.OneSecond);
                Period = CloseTimeUtc - GeneratedTimeUtc;
            }
        }

        /// <summary>
        /// Cancel this insight
        /// </summary>
        /// <param name="utcTime">The algorithm's current time in UTC. See <see cref="IAlgorithm.UtcTime"/></param>
        public void Cancel(DateTime utcTime)
        {
            Expire(utcTime);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Insight"/> class
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="period">The period over which the prediction will come true</param>
        /// <param name="type">The type of insight, price/volatility</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        public Insight(Symbol symbol, TimeSpan period, InsightType type, InsightDirection direction, string tag = "")
            : this(symbol, period, type, direction, null, null, null, null, tag)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Insight"/> class
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="period">The period over which the prediction will come true</param>
        /// <param name="type">The type of insight, price/volatility</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percentage change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">An identifier defining the model that generated this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        public Insight(Symbol symbol, TimeSpan period, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
        {
            Id = Guid.NewGuid();
            Score = new InsightScore();
            SourceModel = sourceModel;

            Symbol = symbol;
            Type = type;
            Direction = direction;
            Period = period;

            // Optional
            Magnitude = magnitude;
            Confidence = confidence;
            Weight = weight;
            Tag = tag;

            _periodSpecification = new TimeSpanPeriodSpecification(period);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Insight"/> class
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="expiryFunc">Func that defines the expiry time</param>
        /// <param name="type">The type of insight, price/volatility</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        public Insight(Symbol symbol, Func<DateTime, DateTime> expiryFunc, InsightType type, InsightDirection direction, string tag = "")
            : this(symbol, expiryFunc, type, direction, null, null, null, null, tag)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Insight"/> class
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="expiryFunc">Func that defines the expiry time</param>
        /// <param name="type">The type of insight, price/volatility</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percentage change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">An identifier defining the model that generated this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        public Insight(Symbol symbol, Func<DateTime, DateTime> expiryFunc, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
            : this(symbol, new FuncPeriodSpecification(expiryFunc), type, direction, magnitude, confidence, sourceModel, weight, tag)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Insight"/> class.
        /// This constructor is provided mostly for testing purposes. When running inside an algorithm,
        /// the generated and close times are set based on the algorithm's time.
        /// </summary>
        /// <param name="generatedTimeUtc">The time this insight was generated in utc</param>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="period">The period over which the prediction will come true</param>
        /// <param name="type">The type of insight, price/volatility</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percentage change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">An identifier defining the model that generated this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        public Insight(DateTime generatedTimeUtc, Symbol symbol, TimeSpan period, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
            : this(symbol, period, type, direction, magnitude, confidence, sourceModel, weight, tag)
        {
            GeneratedTimeUtc = generatedTimeUtc;
            CloseTimeUtc = generatedTimeUtc + period;
        }

        /// <summary>
        /// Private constructor used to keep track of how a user defined the insight period.
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="periodSpec">A specification defining how the insight's period was defined, via time span, via resolution/barcount, via close time</param>
        /// <param name="type">The type of insight, price/volatility</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percentage change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">An identifier defining the model that generated this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        private Insight(Symbol symbol, IPeriodSpecification periodSpec, InsightType type, InsightDirection direction, double? magnitude, double? confidence, string sourceModel = null, double? weight = null, string tag = "")
        {
            Id = Guid.NewGuid();
            Score = new InsightScore();
            SourceModel = sourceModel;

            Symbol = symbol;
            Type = type;
            Direction = direction;

            // Optional
            Magnitude = magnitude;
            Confidence = confidence;
            Weight = weight;
            Tag = tag;

            _periodSpecification = periodSpec;

            // keep existing behavior of Insight.Price such that we set the period immediately
            var period = (periodSpec as TimeSpanPeriodSpecification)?.Period;
            if (period != null)
            {
                Period = period.Value;
            }
        }

        /// <summary>
        /// Sets the insight period and close times if they have not already been set.
        /// </summary>
        /// <param name="exchangeHours">The insight's security exchange hours</param>
        public void SetPeriodAndCloseTime(SecurityExchangeHours exchangeHours)
        {
            if (GeneratedTimeUtc == default(DateTime))
            {
                throw new InvalidOperationException(Messages.Insight.GeneratedTimeUtcNotSet(this));
            }

            _periodSpecification.SetPeriodAndCloseTime(this, exchangeHours);
        }

        /// <summary>
        /// Creates a deep clone of this insight instance
        /// </summary>
        /// <returns>A new insight with identical values, but new instances</returns>
        public virtual Insight Clone()
        {
            return new Insight(Symbol, Period, Type, Direction, Magnitude, Confidence, weight: Weight, tag: Tag)
            {
                GeneratedTimeUtc = GeneratedTimeUtc,
                CloseTimeUtc = CloseTimeUtc,
                Score = Score,
                Id = Id,
                EstimatedValue = EstimatedValue,
                ReferenceValue = ReferenceValue,
                ReferenceValueFinal = ReferenceValueFinal,
                SourceModel = SourceModel,
                GroupId = GroupId
            };
        }

        /// <summary>
        /// Creates a new insight for predicting the percent change in price over the specified period
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="resolution">The resolution used to define the insight's period and also used to determine the insight's close time</param>
        /// <param name="barCount">The number of resolution time steps to make in market hours to compute the insight's closing time</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percent change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">The model generating this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        /// <returns>A new insight object for the specified parameters</returns>
        public static Insight Price(Symbol symbol, Resolution resolution, int barCount, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
        {
            if (barCount < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(barCount), Messages.Insight.InvalidBarCount);
            }

            var spec = new ResolutionBarCountPeriodSpecification(resolution, barCount);
            return new Insight(symbol, spec, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
        }

        /// <summary>
        /// Creates a new insight for predicting the percent change in price over the specified period
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="closeTimeLocal">The insight's closing time in the security's exchange time zone</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percent change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">The model generating this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        /// <returns>A new insight object for the specified parameters</returns>
        public static Insight Price(Symbol symbol, DateTime closeTimeLocal, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
        {
            var spec = closeTimeLocal == Time.EndOfTime ? (IPeriodSpecification)
                new EndOfTimeCloseTimePeriodSpecification() : new CloseTimePeriodSpecification(closeTimeLocal);
            return new Insight(symbol, spec, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
        }

        /// <summary>
        /// Creates a new insight for predicting the percent change in price over the specified period
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="period">The period over which the prediction will come true</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percent change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">The model generating this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        /// <returns>A new insight object for the specified parameters</returns>
        public static Insight Price(Symbol symbol, TimeSpan period, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
        {
            if (period < Time.OneSecond)
            {
                throw new ArgumentOutOfRangeException(nameof(period), Messages.Insight.InvalidPeriod);
            }

            var spec = period == Time.EndOfTimeTimeSpan ? (IPeriodSpecification)
                new EndOfTimeCloseTimePeriodSpecification() : new TimeSpanPeriodSpecification(period);
            return new Insight(symbol, spec, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
        }

        /// <summary>
        /// Creates a new insight for predicting the percent change in price over the specified period
        /// </summary>
        /// <param name="symbol">The symbol this insight is for</param>
        /// <param name="expiryFunc">Func that defines the expiry time</param>
        /// <param name="direction">The predicted direction</param>
        /// <param name="magnitude">The predicted magnitude as a percent change</param>
        /// <param name="confidence">The confidence in this insight</param>
        /// <param name="sourceModel">The model generating this insight</param>
        /// <param name="weight">The portfolio weight of this insight</param>
        /// <param name="tag">The insight's tag containing additional information</param>
        /// <returns>A new insight object for the specified parameters</returns>
        public static Insight Price(Symbol symbol, Func<DateTime, DateTime> expiryFunc, InsightDirection direction, double? magnitude = null, double? confidence = null, string sourceModel = null, double? weight = null, string tag = "")
        {
            return new Insight(symbol, expiryFunc, InsightType.Price, direction, magnitude, confidence, sourceModel, weight, tag);
        }

        /// <summary>
        /// Creates a new, unique group id and sets it on each insight
        /// </summary>
        /// <param name="insights">The insights to be grouped</param>
        public static IEnumerable<Insight> Group(params Insight[] insights)
        {
            if (insights == null)
            {
                throw new ArgumentNullException(nameof(insights));
            }

            var groupId = Guid.NewGuid();
            foreach (var insight in insights)
            {
                if (insight.GroupId.HasValue)
                {
                    throw new InvalidOperationException(Messages.Insight.InsightAlreadyAssignedToAGroup(insight));
                }

                insight.GroupId = groupId;
            }
            return insights;
        }

        /// <summary>
        /// Creates a new, unique group id and sets it on each insight
        /// </summary>
        /// <param name="insight">The insight to be grouped</param>
        public static IEnumerable<Insight> Group(Insight insight) => Group(new[] {insight});

        /// <summary>
        /// Creates a new <see cref="Insight"/> object from the specified serialized form
        /// </summary>
        /// <param name="serializedInsight">The insight DTO</param>
        /// <returns>A new insight containing the information specified</returns>
        public static Insight FromSerializedInsight(SerializedInsight serializedInsight)
        {
            var sid = SecurityIdentifier.Parse(serializedInsight.Symbol);
            var insight = new Insight(
                Time.UnixTimeStampToDateTime(serializedInsight.CreatedTime),
                new Symbol(sid, serializedInsight.Ticker ?? sid.Symbol),
                TimeSpan.FromSeconds(serializedInsight.Period),
                serializedInsight.Type,
                serializedInsight.Direction,
                serializedInsight.Magnitude,
                serializedInsight.Confidence,
                serializedInsight.SourceModel,
                serializedInsight.Weight,
                serializedInsight.Tag
            )
            {
                Id = Guid.Parse(serializedInsight.Id),
                CloseTimeUtc = Time.UnixTimeStampToDateTime(serializedInsight.CloseTime),
                EstimatedValue = serializedInsight.EstimatedValue,
                ReferenceValue = serializedInsight.ReferenceValue,
                ReferenceValueFinal = serializedInsight.ReferenceValueFinal,
                GroupId = string.IsNullOrEmpty(serializedInsight.GroupId) ? (Guid?) null : Guid.Parse(serializedInsight.GroupId)
            };

            // only set score values if non-zero or if they're the final scores
            if (serializedInsight.ScoreIsFinal)
            {
                insight.Score.SetScore(InsightScoreType.Magnitude, serializedInsight.ScoreMagnitude, insight.CloseTimeUtc);
                insight.Score.SetScore(InsightScoreType.Direction, serializedInsight.ScoreDirection, insight.CloseTimeUtc);
                insight.Score.Finalize(insight.CloseTimeUtc);
            }
            else
            {
                if (serializedInsight.ScoreMagnitude != 0)
                {
                    insight.Score.SetScore(InsightScoreType.Magnitude, serializedInsight.ScoreMagnitude, insight.CloseTimeUtc);
                }

                if (serializedInsight.ScoreDirection != 0)
                {
                    insight.Score.SetScore(InsightScoreType.Direction, serializedInsight.ScoreDirection, insight.CloseTimeUtc);
                }
            }

            return insight;
        }

        /// <summary>
        /// Computes the insight closing time from the given generated time, resolution and bar count.
        /// This will step through market hours using the given resolution, respecting holidays, early closes, weekends, etc..
        /// </summary>
        /// <param name="exchangeHours">The exchange hours of the insight's security</param>
        /// <param name="generatedTimeUtc">The insight's generated time in utc</param>
        /// <param name="resolution">The resolution used to 'step-through' market hours to compute a reasonable close time</param>
        /// <param name="barCount">The number of resolution steps to take</param>
        /// <returns>The insight's closing time in utc</returns>
        public static DateTime ComputeCloseTime(SecurityExchangeHours exchangeHours, DateTime generatedTimeUtc, Resolution resolution, int barCount)
        {
            if (barCount < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(barCount), Messages.Insight.InvalidBarCount);
            }

            // remap ticks to seconds
            resolution = resolution == Resolution.Tick ? Resolution.Second : resolution;
            if (resolution == Resolution.Hour)
            {
                // remap hours to minutes to avoid complications w/ stepping through
                // for example 9->10 is an hour step but market opens at 9:30
                barCount *= 60;
                resolution = Resolution.Minute;
            }

            var barSize = resolution.ToTimeSpan();
            var startTimeLocal = generatedTimeUtc.ConvertFromUtc(exchangeHours.TimeZone);
            var closeTimeLocal = Time.GetEndTimeForTradeBars(exchangeHours, startTimeLocal, barSize, barCount, false);
            return closeTimeLocal.ConvertToUtc(exchangeHours.TimeZone);
        }

        /// <summary>
        /// computs the insight closing time from the given generated time and period
        /// </summary>
        /// <param name="exchangeHours">The exchange hours of the insight's security</param>
        /// <param name="generatedTimeUtc">The insight's generated time in utc</param>
        /// <param name="period">The insight's period</param>
        /// <returns>The insight's closing time in utc</returns>
        public static DateTime ComputeCloseTime(SecurityExchangeHours exchangeHours, DateTime generatedTimeUtc, TimeSpan period)
        {
            if (period < Time.OneSecond)
            {
                throw new ArgumentOutOfRangeException(nameof(period), Messages.Insight.InvalidPeriod);
            }

            var barSize = period.ToHigherResolutionEquivalent(false);
            // remap ticks to seconds
            barSize = barSize == Resolution.Tick ? Resolution.Second : barSize;
            // remap hours to minutes to avoid complications w/ stepping through, for example 9->10 is an hour step but market opens at 9:30
            barSize = barSize == Resolution.Hour ? Resolution.Minute : barSize;
            var barCount = (int)(period.Ticks / barSize.ToTimeSpan().Ticks);
            var closeTimeUtc = ComputeCloseTime(exchangeHours, generatedTimeUtc, barSize, barCount);
            if (closeTimeUtc == generatedTimeUtc)
            {
                return ComputeCloseTime(exchangeHours, generatedTimeUtc, Resolution.Second, 1);
            }

            var totalPeriodUsed = barSize.ToTimeSpan().Multiply(barCount);
            if (totalPeriodUsed != period)
            {
                var delta = period - totalPeriodUsed;

                // interpret the remainder as fractional trading days
                if (barSize == Resolution.Daily)
                {
                    var percentOfDay = delta.Ticks / (double) Time.OneDay.Ticks;
                    delta = exchangeHours.RegularMarketDuration.Multiply(percentOfDay);
                }

                if (delta != TimeSpan.Zero)
                {
                    // continue stepping forward using minute resolution for the remainder
                    barCount = (int) (delta.Ticks / Time.OneMinute.Ticks);
                    if (barCount > 0)
                    {
                        closeTimeUtc = ComputeCloseTime(exchangeHours, closeTimeUtc, Resolution.Minute, barCount);
                    }
                }
            }

            return closeTimeUtc;
        }

        /// <summary>Returns a string that represents the current object.</summary>
        /// <returns>A string that represents the current object.</returns>
        /// <filterpriority>2</filterpriority>
        public override string ToString()
        {
            return Messages.Insight.ToString(this);
        }

        /// <summary>
        /// Returns a short string that represents the current object.
        /// </summary>
        /// <returns>A string that represents the current object.</returns>
        public string ShortToString()
        {
            return Messages.Insight.ShortToString(this);
        }


        /// <summary>
        /// Distinguishes between the different ways an insight's period/close times can be specified
        /// This was really only required since we can't properly acces certain data from within a static
        /// context (such as Insight.Price) or from within a constructor w/out requiring the users to properly
        /// fetch the required data and supply it as an argument.
        /// </summary>
        private interface IPeriodSpecification
        {
            void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours);
        }

        /// <summary>
        /// User defined the insight's period using a time span
        /// </summary>
        private class TimeSpanPeriodSpecification : IPeriodSpecification
        {
            public readonly TimeSpan Period;

            public TimeSpanPeriodSpecification(TimeSpan period)
            {
                if (period == TimeSpan.Zero)
                {
                    period = Time.OneSecond;
                }

                Period = period;
            }

            public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
            {
                insight.Period = Period;
                insight.CloseTimeUtc = ComputeCloseTime(exchangeHours, insight.GeneratedTimeUtc, Period);
            }
        }

        /// <summary>
        /// User defined insight's period using a resolution and bar count
        /// </summary>
        private class ResolutionBarCountPeriodSpecification : IPeriodSpecification
        {
            public readonly Resolution Resolution;
            public readonly int BarCount;

            public ResolutionBarCountPeriodSpecification(Resolution resolution, int barCount)
            {
                if (resolution == Resolution.Tick)
                {
                    resolution = Resolution.Second;
                }

                if (resolution == Resolution.Hour)
                {
                    // remap hours to minutes to avoid errors w/ half hours, for example, 9:30 open
                    barCount *= 60;
                    resolution = Resolution.Minute;
                }

                Resolution = resolution;
                BarCount = barCount;
            }

            public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
            {
                insight.CloseTimeUtc = ComputeCloseTime(exchangeHours, insight.GeneratedTimeUtc, Resolution, BarCount);
                insight.Period = insight.CloseTimeUtc - insight.GeneratedTimeUtc;
            }
        }

        /// <summary>
        /// User defined the insight's local closing time
        /// </summary>
        private class CloseTimePeriodSpecification : IPeriodSpecification
        {
            public readonly DateTime CloseTimeLocal;

            public CloseTimePeriodSpecification(DateTime closeTimeLocal)
            {
                CloseTimeLocal = closeTimeLocal;
            }

            public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
            {
                // Prevent close time to be defined to a date/time in closed market
                var closeTimeLocal = exchangeHours.IsOpen(CloseTimeLocal, false)
                    ? CloseTimeLocal
                    : exchangeHours.GetNextMarketOpen(CloseTimeLocal, false);

                insight.CloseTimeUtc = closeTimeLocal.ConvertToUtc(exchangeHours.TimeZone);

                if (insight.GeneratedTimeUtc > insight.CloseTimeUtc)
                {
                    throw new ArgumentOutOfRangeException(nameof(closeTimeLocal), $"Insight closeTimeLocal must not be in the past.");
                }

                insight.Period = insight.CloseTimeUtc - insight.GeneratedTimeUtc;
            }
        }

        /// <summary>
        /// Special case for insights which close time is defined by a function
        /// and want insights to expiry with calendar rules
        /// </summary>
        private class FuncPeriodSpecification : IPeriodSpecification
        {
            public readonly Func<DateTime, DateTime> _expiryFunc;

            public FuncPeriodSpecification(Func<DateTime, DateTime> expiryFunc)
            {
                _expiryFunc = expiryFunc;
            }

            public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
            {
                var closeTimeLocal = insight.GeneratedTimeUtc.ConvertFromUtc(exchangeHours.TimeZone);
                closeTimeLocal = _expiryFunc(closeTimeLocal);

                // Prevent close time to be defined to a date/time in closed market
                if (!exchangeHours.IsOpen(closeTimeLocal, false))
                {
                    closeTimeLocal = exchangeHours.GetNextMarketOpen(closeTimeLocal, false);
                }

                insight.CloseTimeUtc = closeTimeLocal.ConvertToUtc(exchangeHours.TimeZone);
                insight.Period = insight.CloseTimeUtc - insight.GeneratedTimeUtc;
            }
        }

        /// <summary>
        /// Special case for insights where we do not know whats the
        /// <see cref="Period"/> or <see cref="CloseTimeUtc"/>.
        /// </summary>
        private class EndOfTimeCloseTimePeriodSpecification : IPeriodSpecification
        {
            public void SetPeriodAndCloseTime(Insight insight, SecurityExchangeHours exchangeHours)
            {
                insight.Period = Time.EndOfTimeTimeSpan;
                insight.CloseTimeUtc = Time.EndOfTime;
            }
        }
    }
}
